package com.Cfeng.XiaohuanChat.config;

import com.Cfeng.XiaohuanChat.filter.KaptchaCodeFilter;
import com.Cfeng.XiaohuanChat.session.SessionInfoExiredStrategy;
import com.Cfeng.XiaohuanChat.session.SessionInvalidStrategy;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.web.servlet.ServletListenerRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.security.core.session.SessionRegistryImpl;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
import org.springframework.security.web.session.HttpSessionEventPublisher;

import javax.sql.DataSource;

/**
 * @author Cfeng
 * @date 2022/8/6
 * 注册登录安全配置类
 */

@EnableWebSecurity
@RequiredArgsConstructor
@EnableGlobalMethodSecurity(prePostEnabled = true,jsr250Enabled = true,securedEnabled = true) //注解
public class SpringSecurityConfig {

    private final ObjectMapper objectMapper;

    private final UserDetailsService userDetailsService;

    private final DataSource dataSource;
    //自定义的验证码过滤器
    private final KaptchaCodeFilter kaptchaCodeFilter;

    private final SimpleUrlAuthenticationFailureHandler failureHandler;
    /**
     * spring security 配置密码
     */
    @Bean
    public PasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /**
     * 记住我功能的repository
     */
    @Bean
    public PersistentTokenRepository persistentTokenRepository() {
        JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
        tokenRepository.setDataSource(dataSource);
        return tokenRepository;
    }

    /**
     * 全局静态资源放行【动态的路径在http中配置】
     */
    @Bean
    public WebSecurityCustomizer webSecurityCustomizer() {
        return (web) -> web.ignoring().antMatchers("/js/**", "/img/**","/css/**", "/music/**","/editorMd-master/**","/login.html","/logError.html" ,"/accessDenied.html","/register.html","/login/**","/layui/**","/kaptcha");
    }

    /**
     * 认证管理器对象，使用configuration建造即可
     */
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }

    /**
     * Session业务，使用spring session使用
     */
    @Bean
    public SessionRegistry sessionRegistry() {
        //用于访问Session
        return new SessionRegistryImpl();
    }

    @Bean
    public static ServletListenerRegistrationBean httpSessionEventPublisher() {
        //必须要告诉Spring信息将Session存储在sessionRegistry
        return new ServletListenerRegistrationBean(new HttpSessionEventPublisher());
    }

    /**
     * 过滤器链配置
     */
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                //添加过滤器
                .addFilterBefore(kaptchaCodeFilter, UsernamePasswordAuthenticationFilter.class)
                //前台使用frame标签
                .headers()
                .frameOptions()
                .disable()
                .and()
                //httpBasic和csrf禁用
                .httpBasic()
                .disable()
                .csrf()
                .disable()
                //登录表单
                .formLogin()
                .loginPage("/login.html")
                .loginProcessingUrl("/authentication/form")
//                .failureForwardUrl("/logError.html") //该方式登录出错后没有error
//                .failureUrl("/logError.html").permitAll()
                .failureHandler(failureHandler) //设置处理器
                .defaultSuccessUrl("/sys/toIndex",true)  //可以总是跳转
                .usernameParameter("userName")
                .passwordParameter("userPwd")
                .permitAll()
                //配置路径过滤器
                .and()
                .authorizeRequests()
                .antMatchers("/sys/getRegion").permitAll()
                .antMatchers("/sys/getAddress").permitAll()
                .antMatchers("/sys/getErrorMessage").permitAll()
                .anyRequest().authenticated() //登录之后都能访问
                //配置无权处理路径
                .and()
                .exceptionHandling()
                .accessDeniedPage("/accessDenied.html")
                //配置登出路径
                .and()
                .logout()
                .logoutUrl("/logout")
                //配置RememberMe
                .and()
                .rememberMe()
                .rememberMeParameter("rememberMe")
                .rememberMeCookieName("rememberMe")
                .tokenValiditySeconds(5 * 60 * 60)
                .tokenRepository(this.persistentTokenRepository())
                .userDetailsService(userDetailsService)
                //配置登录用户Session
                .and()
                .sessionManagement()
                //登录状态过期的处理，也可以直接配置url
                .invalidSessionStrategy(new SessionInvalidStrategy(objectMapper))
                .maximumSessions(1) //并发上限1
                .maxSessionsPreventsLogin(false)  //true为阻止，false为提出旧的
                //并发上限时被踢出的页面的处理，可以直接跳转url，也可以strategy
                .expiredSessionStrategy(new SessionInfoExiredStrategy(objectMapper)) //session失效监听的处理程序
                .sessionRegistry(sessionRegistry()); //添加注册器

        return http.build();
    }
}
